﻿
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;


namespace Boilen.Primitives.Members {

    /// <summary>
    /// Contains documentation for a <see cref="Member"/>.
    /// </summary>
    public sealed class Doc : IWritable {

        /// <summary>Delimit expandable replacements.</summary>
        public const char ExpanderDelimiter = '%';

        /// <summary>Delimit arguments for expandable replacements.</summary>
        public const char ExpanderArgumentsDelimiter = ':';

        /// <summary>Replaced with the name of the member.</summary>
        public const string Name = "name";

        /// <summary>Replaced with the type of the member.</summary>
        public const string Type = "type";

        /// <summary>Replaced with the name of the parent type.</summary>
        public const string ParentType = "parent_type";


        private static readonly Dictionary<string, string> StandardReplacements = new Dictionary<string, string> {
            { " null", " <see langword='null'/>" },
            { " true", " <see langword='true'/>" },
            { " false", " <see langword='false'/>" },
            { "null ", "<see langword='null'/> " },
            { "true ", "<see langword='true'/> " },
            { "false ", "<see langword='false'/> " },
        };
        private static readonly Dictionary<string, string> StandardExpanders = new Dictionary<string, string> {
            { "see", "<see cref='{0}'/>" },
            { "seealso", "<seealso cref='{0}'/>" },
            { "langword", "<see langword='{0}'/>" },
            { "paramref", "<paramref name='{0}'/>" },
            { "typeparamref", "<typeparamref name='{0}'/>" },
            { "inherit", "<inheritdoc/>" },
            { "inheritfrom", "<inheritdoc cref='{0}'/>" },
        };


        private readonly Dictionary<string, string> replacements_;
        private readonly Dictionary<string, string> expanders_;
        private readonly List<DocElement> docElements_ = new List<DocElement>( );
        private readonly string fullyQualifiedName_;
        private readonly string includeType_;
        private Options options_;


        /// <summary>
        /// Gets or sets the member to inherit from.
        /// </summary>
        /// <remarks>
        /// If set to <null/>, the <c>inherit</c> element will not be added.
        /// If set to an empty <see cref="string"/>, the <c>inherit</c> element will be added without the <c>from</c> attribute.
        /// Otherwise, the <c>inherit</c> element will be added with the <c>from</c> attribute set to the specified value.
        /// </remarks>
        public string InheritFrom { get; set; }

        /// <summary>
        /// Gets the fully-qualified name of the member being documented.
        /// </summary>
        public string FullyQualifiedName { get { return this.fullyQualifiedName_; } }


        public Doc( string fullyQualifiedName, string name, string type, string parentType )
            : this( fullyQualifiedName, parentType ?? name, StandardReplacements, StandardExpanders ) {
            Ensure.NotNullOrEmpty( name );
            Ensure.NotNullOrEmpty( type );

            this.expanders_[Doc.Name] = name;
            this.expanders_[Doc.Type] = GetDocTypeName( type );
            this.expanders_[Doc.ParentType] = GetDocTypeName( parentType ?? "" );
        }

        public Doc( Doc doc ) : this( doc.fullyQualifiedName_, doc.includeType_, doc.replacements_, doc.expanders_ ) { }

        private Doc( string fullyQualifiedName, string includeType, IDictionary<string, string> initialReplacements, IDictionary<string, string> initialExpanders ) {
            Ensure.NotNullOrEmpty( fullyQualifiedName );

            this.fullyQualifiedName_ = fullyQualifiedName;
            this.replacements_ = new Dictionary<string, string>( initialReplacements );
            this.expanders_ = new Dictionary<string, string>( initialExpanders );
            this.includeType_ = includeType.Replace( '<', '`' ).Replace( ", ", "," ).Replace( ">", "" );
        }


        /// <summary>
        /// Updates the documentation settings to use an include tag.
        /// </summary>
        public Doc UseIncludeTag( ) {
            this.options_ |= Options.UseIncludeTag;
            return this;
        }

        /// <summary>
        /// Updates the documentation settings to use the documentation members.
        /// </summary>
        public Doc UseDocMembers( bool onlyDocMembers = false ) {
            if( onlyDocMembers )
                this.options_ = Options.UseDocMembers;
            else
                this.options_ |= Options.UseDocMembers;
            return this;
        }

        /// <summary>
        /// Updates the documentation settings based on the state of the child documentation elements.
        /// </summary>
        public void UpdateSettings( IEnumerable<Doc> children ) {
            if( children == null )
                return;

            foreach( Doc child in children )
                if( child != null )
                    this.options_ |= child.options_;

            if( this.options_ == Options.UseIncludeTag )
                foreach( Doc child in children )
                    if( child != null )
                        child.options_ = Options.NoDocMembers;
        }


        /// <summary>
        /// Gets the name of a type that can be used in <c>see</c> documentation elements.
        /// </summary>
        public static string GetDocTypeName( string typeName, bool? showNumberedGenericArguments = true ) {
            Ensure.NotNull( typeName );

            // Replace angle brackets on generic types with braces and type arguments with general names.
            int genericStart = typeName.IndexOf( '<' );
            if( genericStart > 0 && showNumberedGenericArguments.HasValue ) {
                string name = typeName.Substring( 0, genericStart );
                int argumentsLength = typeName.Length - genericStart - 2;
                string arguments = typeName.Substring( genericStart + 1, argumentsLength );

                int parameterCount = 1;
                int nestedParameter = 0;
                foreach( char c in arguments ) {
                    switch( c ) {
                        case ',':
                            if( nestedParameter == 0 )
                                ++parameterCount;
                            break;
                        case '<':
                            ++nestedParameter;
                            break;
                        case '>':
                            --nestedParameter;
                            break;
                        default:
                            break;
                    }
                }

                if( showNumberedGenericArguments == true ) {
                    string genericArguments = parameterCount == 1 ? "T" : string.Join( ",", Enumerable.Range( 0, parameterCount ).Select( i => "T" + i ) );
                    typeName = name + "{" + genericArguments + "}";
                }
                else {
                    typeName = name + "`" + parameterCount;
                }
            }
            else if( genericStart > 0 ) {
                typeName = typeName.Replace( '<', '{' ).Replace( '>', '}' ).Replace( " ", "" );
            }

            return typeName;
        }


        /// <summary>
        /// Adds a replacement replacement value.
        /// </summary>
        public Doc AddReplacement( string key, string format, params object[] args ) {
            Ensure.NotNullOrEmpty( key );
            Ensure.NotNull( format );

            string value = string.Format( format, args );
            this.replacements_[key] = value;
            return this;
        }

        /// <summary>
        /// Adds the specified replacement replacement values.
        /// </summary>
        public Doc AddReplacements( ReplacementsDictionary replacements ) {
            Ensure.NotNull( replacements );

            foreach( var replacement in replacements )
                this.AddReplacement( replacement.Key, replacement.Value );
            return this;
        }

        /// <summary>
        /// Adds an expandable replacement value.
        /// </summary>
        public Doc AddExpander( string key, string format ) {
            Ensure.NotNullOrEmpty( key );
            Ensure.NotNull( format );

            this.expanders_[key] = format;
            return this;
        }

        /// <summary>
        /// Applies replacements to the specified string.
        /// </summary>
        public string ApplyReplacements( string value ) {
            Ensure.NotNull( value );

            var sb = new StringBuilder( );

            // Replace any replacement values resulting in expanders.
            foreach( var replacement in this.replacements_ )
                if( replacement.Value.Contains( ExpanderDelimiter ) )
                    value = value.Replace( replacement.Key, replacement.Value );

            // Expand each delimited value.
            int lastDelimiter = 0;
            int startDelimiter = value.IndexOf( ExpanderDelimiter, lastDelimiter );
            while( startDelimiter >= 0 ) {
                // Append intermediate text between delimiters.
                sb.Append( value.Substring( lastDelimiter, startDelimiter - lastDelimiter ) );

                // Expand text between delimiters.
                int endDelimiter = value.IndexOf( ExpanderDelimiter, startDelimiter + 1 );
                Ensure.Satisfies( endDelimiter > 0, "Could not find end delimiter for start delimiter at {0} in string '{1}'.", startDelimiter, value );
                string delimitedText = value.Substring( startDelimiter + 1, endDelimiter - startDelimiter - 1 );
                string[] parts = delimitedText.Split( new[] { ExpanderArgumentsDelimiter }, StringSplitOptions.RemoveEmptyEntries );
                if( parts.Length == 0 ) {
                    sb.Append( ExpanderDelimiter );
                }
                else {
                    string expanderKey = parts[0];
                    string format = this.expanders_[expanderKey];

                    // Expand any arguments.
                    string[] arguments = parts.Skip( 1 ).ToArray( );
                    for( int i = 0; i < arguments.Length; ++i ) {
                        string[] argParts = arguments[i].Split( new[] { '.' }, 2 );
                        string argRoot = argParts[0];
                        if( this.expanders_.ContainsKey( argRoot ) )
                            argParts[0] = this.expanders_[argRoot];
                        arguments[i] = string.Join( ".", argParts );
                    }

                    sb.AppendFormat( format, arguments );
                }

                // Check for next delimiter.
                lastDelimiter = endDelimiter + 1;
                startDelimiter = value.IndexOf( ExpanderDelimiter, lastDelimiter + 1 );
            }

            // Append any text after last delimiter.
            sb.Append( value.Substring( lastDelimiter ) );

            // Replace primitive values.
            foreach( var replacement in this.replacements_ )
                sb.Replace( replacement.Key, replacement.Value );

            return sb.ToString( );
        }


        /// <summary>
        /// Adds a named documentation element with the specified content.
        /// </summary>
        /// <remarks>
        /// Adding multiple elements with the same name will output a single doc element with multiple <c>para</c> sections.
        /// </remarks>
        public Doc AddDocElement( string name, string format, params object[] args ) {
            if( format == null ) {
                this.options_ |= Options.UseIncludeTag;
                return this;
            }

            Ensure.NotNullOrEmpty( name );
            Ensure.NotNullOrEmpty( format );

            string value = args.Length > 0 ? string.Format( format, args ) : format;
            this.docElements_.Add( new DocElement( this, name, value ) );

            return this;
        }

        /// <summary>
        /// Adds documentation elements created from existing xml documentation.
        /// </summary>
        public Doc AddDocElements( IEnumerable<XElement> elements ) {
            Ensure.NotNull( elements );

            var validElements = XmlDocumentation.FilterElements( elements, "filterpriority" );
            foreach( var element in validElements ) {
                string elementName = XmlDocumentation.GetElementName( element );
                string elementValue = XmlDocumentation.GetElementValue( element );

                this.AddDocElement( elementName, elementValue );
            }

            return this;
        }

        /// <summary>
        /// Adds a <c>summary</c> documentation element with the specified content.
        /// </summary>
        public Doc AddSummary( string format, params object[] args ) {
            if( args.Length == 1 && args[0] == null ) {
                this.options_ |= Options.UseIncludeTag;
                return this;
            }

            string actionParameter = args.FirstOrDefault( ) as string ?? "";

            if( actionParameter.StartsWith( "%lookup:" ) ) {
                string lookup = actionParameter.Split( new[] { ExpanderArgumentsDelimiter }, 2 )[1];

                int memberNameIndex = lookup.LastIndexOf( '.' );
                string lookupTypeName = lookup.Substring( 0, memberNameIndex );
                string lookupMemberName = lookup.Substring( memberNameIndex + 1 );

                Type lookupType = AppDomain.CurrentDomain.GetAssemblies( )
                    .Select( a => a.GetType( lookupTypeName, false, true ) )
                    .FirstOrDefault( t => t != null );
                if( lookupType == null ) { throw new InvalidOperationException( "Could not find type for member " + lookup ); }

                var lookupMember = lookupType.GetMember( lookupMemberName ).First( );

                var lookupDoc = XmlDocumentation.GetDocMember( lookupMember );
                this.AddDocElements( lookupDoc.Elements( ) );
            }
            else if( actionParameter == "%inherit%" )
                this.InheritFrom = "";
            else
                this.AddDocElement( "summary", format, args );

            return this;
        }

        /// <summary>
        /// Adds a <c>returns</c> documentation element with the specified content.
        /// </summary>
        public Doc AddReturns( string format, params object[] args ) {
            return this.AddDocElement( "returns", format, args );
        }

        /// <summary>
        /// Adds a <c>param</c> documentation element with the specified content.
        /// </summary>
        public Doc AddParam( string paramName, string format, params object[] args ) {
            Ensure.NotNullOrEmpty( paramName );

            string name = string.Format( "param name='{0}'", paramName );
            return this.AddDocElement( name, format, args );
        }

        /// <summary>
        /// Adds an <c>exception</c> documentation element with the specified content.
        /// </summary>
        public Doc AddException( string exceptionTypeName, string format, params object[] args ) {
            Ensure.NotNullOrEmpty( exceptionTypeName );

            string name = string.Format( "exception cref='{0}'", exceptionTypeName );
            return this.AddDocElement( name, format, args );
        }

        /// <summary>
        /// Adds <c>exception</c> documentation elements for each of the specified <see cref="Guard"/>s.
        /// </summary>
        public Doc AddExceptions( IEnumerable<Guard> guards ) {
            Ensure.NotNull( guards );

            foreach( var guard in guards )
                if( !guard.IsEmpty )
                    this.AddException( guard.ExceptionType.FullName, guard.Description );

            return this;
        }


        /// <inheritdoc/>
        public void Write( ICodeWriter writer ) {
            using( writer.PushIndent( "/// " ) ) {
                if( this.options_ == Options.None || this.options_.HasFlag( Options.UseDocMembers ) ) {
                    // Write an "inherit" element, if it is set.
                    if( this.InheritFrom != null ) {
                        string inheritFrom = this.ApplyReplacements( this.InheritFrom );
                        string crefAttribute = inheritFrom.Length > 0 ? " cref='" + inheritFrom + "'" : "";

                        writer.WriteLine( "<inheritdoc{0}/>", crefAttribute );
                    }

                    // Write each doc element, writing out values with the same name under one element separated by "para" tags.
                    var groupedElements = this.docElements_.GroupBy( e => e.Name, e => e.Value );
                    foreach( var group in groupedElements ) {
                        string attributedElementName = group.Key;
                        string elementName = attributedElementName.Split( ' ' ).First( );

                        Action<int, bool> separate = delegate { };
                        if( elementName == "exception" ) {
                            // Separate each exception cause by "-or-".
                            const string SeparatorValue = "<para>-or-</para>";
                            separate = ( i, last ) => writer.WriteLine( SeparatorValue );
                        }

                        Action<int, string> operate = ( i, value ) => {
                            if( i == 0 )
                                writer.WriteLine( value );
                            else
                                using( Enclose.Format( writer, "<para>", "</para>" ) )
                                using( writer.PushIndent( "  " ) )
                                    writer.WriteLine( value );
                        };

                        using( Enclose.Format( writer, "<{0}>", "</{1}>", attributedElementName, elementName ) )
                            Util.Iterate( group, separate, operate );
                    }
                }

                // Write include tag, if needed.
                if( this.options_.HasFlag( Options.UseIncludeTag ) ) {
                    writer.WriteLine(
                        "<include file='{0}{1}.xml' path='/doc/member[@name=\"{2}\"]/*' />",
                        GlobalSettings.ExternalDocumentationPrefix, this.includeType_, this.fullyQualifiedName_
                    );
                }
            }
        }


        [Flags]
        private enum Options {
            None = 0,
            UseDocMembers = 1 << 0,
            UseIncludeTag = 1 << 1,
            NoDocMembers = 1 << 2,
        }

        public sealed class ReplacementsDictionary : IEnumerable<KeyValuePair<string, string>> {
            private readonly Dictionary<string, string> replacements_ = new Dictionary<string, string>( );

            public ReplacementsDictionary Add( string key, string format, params object[] args ) {
                string value = string.Format( format, args );
                this.replacements_.Add( key, value );
                return this;
            }

            public IEnumerator<KeyValuePair<string, string>> GetEnumerator( ) { return this.replacements_.GetEnumerator( ); }
            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator( ) { return this.GetEnumerator( ); }
        }

        private sealed class DocElement {
            private readonly Doc parent_;
            private readonly string name_;
            private readonly string rawValue_;

            public string Name { get { return this.name_; } }
            public string Value { get { return this.parent_.ApplyReplacements( this.rawValue_ ); } }

            public DocElement( Doc parent, string name, string rawValue ) {
                Ensure.NotNull( parent );
                Ensure.NotNullOrEmpty( name );
                Ensure.NotNullOrEmpty( rawValue );

                this.parent_ = parent;
                this.name_ = name;
                this.rawValue_ = rawValue;
            }
        }

    }

}
